-- ===========================
-- This file turns into main.lua, which acts as a game in the LOVE framework.
-- Double-clicking the .love file in each session folder allows the user to
-- watch the replay 
-- ===========================
OrunjReplay = {
	sessionID = nil,
	session = nil,
	play = false,
	loadSuccess = false,
		
	drawMEC = true, 	-- Determine whether or not to compute and draw the MEC.
	
	resetRequest = false,
	loop = true,        -- loop flag.
	
	stepRequest = false,
	forwardStep = true,
	currentFrame = 1, 	-- the current frame.
	maxFrame = nil, 	-- maximum number of frames.
	stepSize = 1, 		-- number of frames to step.
	waitTime = 0.1, 	waitTimeIncrement = 0.1,	-- in seconds.
	elapsed = 0,  		-- in seconds.
	
	trailLength = 30, 	-- The length of the trail.
	currentMap = "no map",
	
	
	-- ===========================
	-- keyReleased Callbacks
	-- ===========================
	keyReleasedCallbacks = setmetatable({
		-- Quit the program.
		["escape"] = 
			function() 
				love.event.push('q') 
			end,
		
		-- Toggle the Minimum Enclosing Circle computation/display.
		["c"] = 
			function() 
				OrunjReplay.drawMEC = not OrunjReplay.drawMEC 
			end,
		
		-- Pause/unpause
		[" "] = -- (spacebar)
			function() 
				OrunjReplay.play = not OrunjReplay.play 
			end,
			
		-- Reset to the first frame.
		["r"] = 
			function() 
				OrunjReplay.resetRequest = true 
			end,
		
		-- Toggle looping playback.
		["l"] = 
			function()
				OrunjReplay.loop = not OrunjReplay.loop
			end,
			
		-- Set the metatable so that if any other keys are pressed, nothing happens.
		}, {__index = function(table, key)
				return function() return nil end
			end
		}
	),
	
	-- ===========================
	-- keyPressed Callbacks
	-- ===========================
	keyPressedCallbacks = setmetatable({
		-- Request to step forward once.
		["right"] = 
			function() 
				OrunjReplay.stepRequest = true 
				OrunjReplay.forwardStep = true
			end,
		
		-- Request to step backwards once.
		["left"] =
			function()
				OrunjReplay.stepRequest = true
				OrunjReplay.forwardStep = false
			end,
			
		-- Increase the wait time.
		["up"] = 
			function() 
				OrunjReplay.waitTime = OrunjReplay.waitTime + OrunjReplay.waitTimeIncrement
			end,
			
		-- Decrease the wait time.
		["down"] = 
			function() 
				OrunjReplay.waitTime = math.max(OrunjReplay.waitTime - OrunjReplay.waitTimeIncrement, 0)
			end,
		
		-- Set the metatable so that if any other keys are pressed, nothing happens.
		}, {__index = function(table, key)
				return function() return nil end
			end
		}
	),
	
	-- ===========================
	-- keyDownCallbacks
	-- ===========================
	keyDownCallbacks = setmetatable({
		-- Set the metatable so that if any other keys are pressed, nothing happens.
		}, {__index = function(table, key)
				return function() return nil end
			end
		}
	),

	-- ===========================
	-- mousePressedCallbacks 
	-- ===========================
	mousePressedCallbacks = setmetatable({
		-- Set the metatable so that if any other keys are pressed, nothing happens.
		["wd"] = -- Mouse wheel down.
			function()
				OrunjReplay.stepSize = math.max(OrunjReplay.stepSize - 1, 1)
			end,
		["wu"] = -- Mouse wheel up.
			function()
				OrunjReplay.stepSize = math.floor(math.min(OrunjReplay.stepSize + 1, OrunjReplay.maxFrame / 2))
			end,
		}, {__index = function(table, key)
				return function() return nil end
			end
		}
	), 
}


-- ===========================
-- Class definition for a player, which can be drawn to the canvas.
-- ===========================
PlayerPoint = {
 	-- The list of currently tracked players, i.e. the ones created by
 	-- the New() function.
	players = {},
	
	-- ===========================
	-- The class colors
	-- ===========================
	classColors = {
		["HUNTER"] = {r = 171, g = 212, b = 115},
		["WARLOCK"] = {r = 148, g = 130, b = 201},
		["PRIEST"] = {r = 255, g = 255, b = 255},
		["PALADIN"] = {r = 245, g = 140, b = 186},
		["MAGE"] = {r = 105, g = 204, b = 240},
		["ROGUE"] = {r = 255, g = 245, b = 105},
		["DRUID"] = {r = 255, g = 125, b = 10},
		["SHAMAN"] = {r = 36, g = 89, b = 255},
		["WARRIOR"] = {r = 199, g = 156, b = 110},
		["DEATHKNIGHT"] = {r = 196, g = 30, b = 59},
	},
	
	roleColors = {
		["dps"] = {r = 255, g = 103, b = 53},
		["tank"] = {r = 86, g = 182, b = 255},
		["heals"] = {r = 76, g = 255, b = 0},
	},
	
	-- ===========================
	-- Create a new PlayerPoint and add it to the players array.
	-- ===========================
	New = function(self, orunjPlayer)
		local p = {}
		-- The following setmetatable call essentially makes it such that
		-- "p" inherits the Update and Draw methods of the PlayerPoint table.
		p = setmetatable({}, {__index = self})
		p.player = orunjPlayer
		--p.color = self.classColors[p.player.englishClass]
		p.position = {x = 0, y = 0}
		p.drawable = false
		return p
	end,
	
	-- ===========================
	-- [Static function] Update all the players.
	-- ===========================
	UpdateAll = function(self, frameNumber)
		for guid, player in pairs(self.players)	do
			player:Update(frameNumber)
		end
	end,
	
	-- ===========================
	-- Update the position of the PlayerPoint according to a frame number.
	-- ===========================
	Update = function(self, frameNumber)
		-- Store the frame number to draw the tail later on.
		self.frame = frameNumber
		
		-- Get the x,y positions.
		local x = self.player.timelines.xPositionTimeline[self.frame]
		local y = self.player.timelines.yPositionTimeline[self.frame]
		
		-- Offset the x and y positions by the width and the height of the
		-- window so that everything is always proportional, regardless of the 
		-- user resizing the window.
		if x then self.position.x = x  end
		if y then self.position.y = y  end
		
		-- If the PlayerPoint's position is (0,0) or some other non-sense
		-- value, then he is not in the map, so flag it.
		self.inMap = not (x <= 0 and y <= 0)
	end,

	-- ===========================
	-- [Static function] Draw all the players.
	-- ===========================	
	DrawAll = function(self)	
		-- Draw the player trail first.
		for guid, player in pairs(self.players) do
			player:DrawTrail()
		end
		-- Draw the player point after, so that it goes over the trails.
		for guid, player in pairs(self.players) do
			player:Draw()
		end
	end,
	
	-- ===========================
	-- Draw the PlayerPoint to the canvas.
	-- ===========================
	Draw = function(self)
		-- If we aren't in the map, then don't draw.
		if not self.inMap then return end
		
		love.graphics.setPointStyle("smooth")
		local white = {r = 255, g = 255, b = 255}
		local x, y = self.position.x * love.graphics.getWidth(), self.position.y * love.graphics.getHeight()
		local c
		
		-- Set the color and other attributes of the point.
		c = self.classColors[self.player.class]
		love.graphics.setColor(c.r, c.g, c.b)
		love.graphics.setPointSize(10)
		love.graphics.point(x, y)
		
		-- Set a small ring around the role color.
		love.graphics.setColor(white.r, white.g, white.b)
		love.graphics.setPointSize(8)
		love.graphics.point(x, y)
		
		-- Set the role color.
		c = self.roleColors[self.player.role]
		love.graphics.setColor(c.r, c.g, c.b)
		love.graphics.setPointSize(6)
		love.graphics.point(x, y)
	end,
	
	
	-- ===========================
	-- Draw the trail of the point.
	-- ===========================
	DrawTrail = function(self)
		-- If we aren't in the map, then don't draw.
		if not self.inMap or OrunjReplay.trailLength <= 0 then return end
		
		-- Get the line color.
		local c = self.classColors[self.player.class]
		local maxLineWidth = 3
		
		-- Iterate and draw the trail.
		for i = 1, OrunjReplay.trailLength do
			-- The further we are in the trail length, the less visible the line becomes.
			local ageFactor = (OrunjReplay.trailLength - i + 1) / OrunjReplay.trailLength
			local alpha = math.floor(255 * ageFactor)
			local lineWidth = maxLineWidth --* ageFactor
			
			-- Set up the first frame point. (first point in the line)
			local frame1 = self.frame - i + 1
			if frame1 <= 1 then break end -- We are done drawing the trail if we cannot go back.
			-- Get the frame1's (x,y) position.
			local x1 = self.player.timelines.xPositionTimeline[frame1] * love.graphics.getWidth()
			local y1 = self.player.timelines.yPositionTimeline[frame1] * love.graphics.getHeight()
			if x1 == 0 and y1 == 0 then break end -- Clearly we are off the map so don't draw any more of the trail.
			
			-- Set up the second frame point. (second point in the line)
			local frame2 = self.frame - i
			local x2 = self.player.timelines.xPositionTimeline[frame2] * love.graphics.getWidth()
			local y2 = self.player.timelines.yPositionTimeline[frame2] * love.graphics.getHeight()
			if x2 == 0 and y2 == 0 then break end -- We're off the map again, so stop drawing the trail.
			
			-- Set up the appropriate line state and color.
			love.graphics.setColor(c.r, c.g, c.b, alpha)
			love.graphics.setLineStyle("smooth")
			--love.graphics.setLineWidth(3)
			love.graphics.setLineWidth(lineWidth)
			love.graphics.line(x1, y1, x2, y2)
		end
	end,
}

-- ===========================
-- Get the string representation of the replay state.
-- ===========================
function OrunjReplay:ToString()
	local output = 
		"Orunj session: " .. tostring(self.sessionID) .. "\n" ..
		"Sequence number: " .. tostring(self.currentFrame) .. " / " .. tostring(self.maxFrame) .. "\n" ..
		"Step size: " .. tostring(self.stepSize) .. "\n" ..
		--"Elapsed: " .. tostring(self.elapsed) .. "\n" ..
		"Loop: " .. tostring(self.loop) .. "\n" ..
		"Wait time between updates: " .. tostring(self.waitTime) .. "sec \n" ..
		"Map name: " .. tostring(self.currentMap) .. "\n" ..
		"Scale: x = 1:" .. tostring(zoneScalingData[self.currentMap][1]) .. ", y = 1:" .. tostring(zoneScalingData[self.currentMap][2]) .. "\n" ..
		""
	
	-- Indicate if the system is paused or not.
	if not self.play then
		output = output .. "- paused -\n"
	end
	return output
end

-- ===========================
-- Create and sort the map timeline, so we can easily search 
-- through when the map changes.
-- ===========================
function OrunjReplay:SortMapTimeline()
	-- Set the current map.
	self.mapTimeline = {}
	self.reverseMapTimeline = {}
	for frameNumber, mapName in pairs(self.session.globalTimelines.mapTimeline) do
		table.insert(self.mapTimeline, {frame = frameNumber, map = mapName})
	end
	table.sort(self.mapTimeline, function(a,b) return a.frame < b.frame end)
	
	print("Sorted Map Timeline:")
	for i, tuple in ipairs(self.mapTimeline) do
		print("self.mapTimeline[" .. tostring(i) .. "] = {" .. tostring(tuple.frame) .. ", " .. tostring(tuple.map) .. "}")
	end
	
	self.currentMap = self.mapTimeline[self.currentFrame].map
end

-- ===========================
-- Search in the current directory for an OrunjSave file
-- for example: 07.25.10@18-05-27.lua
-- ===========================
function OrunjReplay:LoadOrunjSave()
	-- Set the window title.
	love.graphics.setCaption("OrunjReplay")
	
	-- Go through the current directory and list its contents.
	-- Here, we need to use LOVE's filesystem API, since we can't
	-- directly call the operating system's os.execute() function. This makes sense
	-- since LOVE only allows you to modify the contents of your own game
	-- directory, and the saving area in the /users/ directory.
	-- [http://love2d.org/wiki/love.filesystem.enumerate]
	local filesTable = love.filesystem.enumerate("")
	local s
	for i, file in ipairs(filesTable) do
		-- Find if the filename matches our OrunjSave file format.
		s = string.match(file, "%d%d%.%d%d%.%d%d@%d%d%-%d%d%-%d%d%.lua") 
			or string.match(file, "MapSizes.lua")
			or string.match(file, "MEC.lua")
		print(s)
		if love.filesystem.isFile(file) and s then
			-- Deserialize the contents of the file.
			local fn = love.filesystem.load(s)
			fn()
		end
	end
	
	-- Make sure the OrunjSave table exists after deserialization.
	if OrunjSave then
		-- Load the first OrunjSave session into OrunjReplay
		for sessionID, session in pairs(OrunjSave.sessions) do
			self.sessionID = sessionID
			self.session = session
			self.maxFrame = session.sequenceNumber - 1
			break -- Leave after the first pair.
		end
	end

	-- Return the filename of the OrunjSave file as proof of its existence.
	self.loadSuccess = OrunjSave
	
	-- If the load success failed, then we are done.
	if not self.loadSuccess then return end
	
	-- Otherwise, load the players into the PlayerPoint table.
	for guid, player in pairs(self.session.players) do
		local p = PlayerPoint:New(player)
		PlayerPoint.players[guid] = p
	end
	
	self:SortMapTimeline()
	
	self.mec = self.mec or MEC:New()
	--[[ DEBUG
	for k, v in pairs(self.mapTimeline) do
		print(tostring(k) .. ": " .. tostring(v))
	end
	]]--
end

-- ===========================
-- Called by the LOVE framework to load any stuff before starting.
-- ===========================
function love.load()
	-- Load the contents of the OrunjSave table.
	OrunjReplay:LoadOrunjSave() 
end

-- ===========================
-- Compute the minimum enclosing circle for the current frame.
-- ===========================
function OrunjReplay:ComputeMEC()

	-- Extract the set of points for this frame.
	local points = {}
	for guid, player in pairs(PlayerPoint.players) do
		if player.inMap and player.position then
			table.insert(points, player.position)
		end
	end
	-- Set the points for the MEC algorithm.
	self.mec:SetPoints(points)
	-- Compute the MEC using the set of points.
	-- The result is in self.mec.circle
	self.mec:Compute()
end

-- ===========================
-- Check if any keys are being held down.
-- ===========================
function OrunjReplay:CheckForKeysDown()
	for key, func in pairs(self.keyDownCallbacks) do
		if love.keyboard.isDown(key) then
			func()
		end
	end
end

-- ===========================
-- Detect a map change.
-- ===========================
function OrunjReplay:DetectMapChange(nextFrame)
	for i, tuple in ipairs(self.mapTimeline) do
		if tuple.frame <= nextFrame then
			self.currentMap = tuple.map
		end
	end
end

-- ===========================
-- Update replay state every frame.
-- ===========================
function OrunjReplay:Update(dt)
	
	-- Check if we need to reset to the first frame.
	if self.resetRequest then
		self.currentFrame = 1
		-- Switch the reset request back to false.
		self.resetRequest = false
	end
	
	-- Compute the elapsed time since the last update.
	self.elapsed = self.elapsed + dt
	
	-- Increment the current frame if we need to, if we are
	-- playing the replay, or if we have a step request.
	if self.play or self.stepRequest then	
		if self.stepRequest or self.elapsed > self.waitTime then
			-- Step in the appropriate direction (forward or backwards)
			local step = self.forwardStep and self.stepSize or (-1) * self.stepSize
			--self.forwardStep = true
			
			-- Determine the next frame
			local nextFrame = self.currentFrame + step
			
			-- See if we need to loop.
			if self.loop then
				if nextFrame > self.maxFrame then
					--nextFrame = 1
					nextFrame = nextFrame - self.maxFrame
				elseif nextFrame < 1 then
					--nextFrame = self.maxFrame
					nextFrame = self.maxFrame - math.abs(nextFrame)
				end
			end
			
			-- Change the current frame, while keeping track of the previous one.
			--local previousFrame = self.currentFrame
			self.currentFrame = math.max(math.min(nextFrame, self.maxFrame), 1)
			
			-- Detect a map change.
			--OrunjReplay:DetectMapChange(previousFrame, self.currentFrame)
			self:DetectMapChange(self.currentFrame)
			
			-- Update all the players.
			PlayerPoint:UpdateAll(self.currentFrame)
			
			-- Compute the minimum enclosing circle.
			self:ComputeMEC()
			
			-- reset our elapsed time counter.
			self.elapsed = 0
		end
		
		-- Switch the step request to false.
		self.stepRequest = false
	end
end

-- ===========================
-- Called by the LOVE framework to update internal logic.
-- dt is the time since the last update in seconds.
-- ===========================
function love.update(dt)
	-- Check for key input.
	OrunjReplay:CheckForKeysDown()

	-- If we have successfully loaded, update the internal replay state.
	if OrunjReplay.loadSuccess then
		OrunjReplay:Update(dt)
	end
end

-- ===========================
-- Draw the minimum enclosing circle in white.
-- ===========================
function OrunjReplay:DrawMEC()
	if not self.mec.Draw then
		self.mec.Draw = function(self)
			if not self.circle then return end
			local circle = self.circle
			local x = circle.center.x * love.graphics.getWidth()
			local y = circle.center.y * love.graphics.getHeight()
			--local radius = circle.radius * love.graphics.getWidth()
			local dx = (circle.center.x - circle.radiusPoint.x) * love.graphics.getWidth()
			local dy = (circle.center.y - circle.radiusPoint.y) * love.graphics.getHeight()
			local radius = math.sqrt(dx*dx + dy*dy)
			
			love.graphics.setColor(255,255,255,255/2)
			love.graphics.setLine(1.5)
			love.graphics.circle("line", x, y, radius, 50) 
		end
	end
	self.mec:Draw()
end

-- ===========================
-- Draw the replay stuff to the screen.
-- ===========================
function OrunjReplay:Draw()
	-- Draw the minimum enclosing circle
	OrunjReplay:DrawMEC()

	-- Draw all the players
	PlayerPoint:DrawAll()

	-- Reset the color to white before drawing text.
	love.graphics.setColor(255,255,255,255)
	local output = self:ToString()
	--love.graphics.print(output, love.graphics.getWidth() / 5 , love.graphics.getHeight() / 5)
	love.graphics.print(output, 0, love.graphics.getHeight() / 50)
end

-- ===========================
-- Called by the LOVE framework to draw the contents to the screen.
-- ===========================
function love.draw()
	if OrunjReplay.loadSuccess then
		OrunjReplay:Draw()
	else
		love.graphics.print("Failed to load OrunjReplay.", love.graphics.getWidth() / 5 , love.graphics.getHeight() / 5)	
	end
end

-- ===========================
-- Keyboard callback functions.
-- ===========================
function love.keyreleased(key, unicode)
	OrunjReplay.keyReleasedCallbacks[key]()
end

function love.keypressed(key, unicode)
	OrunjReplay.keyPressedCallbacks[key]()
end

function love.mousepressed(x, y, button)
	OrunjReplay.mousePressedCallbacks[button]()
end